package ex1.model;

import java.awt.Color; //TODO : you don't need to do anything but use this class - find the API via google 
import java.awt.image.BufferedImage;
//import java.awt.image.ColorModel;

/**
 * Contains a few handy image processing routines
 */
public class ImageProcessor
{

    public static final double MAX_COLOR_SCALE = 255.0;
    
    public static double[][] deriveKernel = { };//TODO: 
    
    public static double[][] deriveKernel2 = { };//TODO: 
    
    public static double[][] gaussianBlur = { };//TODO: add this only for testing

    public static double[][] m_OriginalImageRed;
    public static double[][] m_OriginalImageGreen;
    public static double[][] m_OriginalImageBlue;
    
    /**
     * Applies a Convolution operator to a given matrix and kernel.
     * 
     * @param I
     *            A 2d array
     * @param kernel
     *            The kernel/filter. Must be square and have odd size!
     * @return A new 2D array not necessarily of the size as I
     */
    public static double[][] convolve(double[][] I, double[][] kernel)
    {
        int KernelSize = kernel.length;
        int ImageRows = I.length;
        int ImageCols = I[0].length;
        double[][] Output = new double[ImageRows][ImageCols];
        int KernelBorder = (KernelSize - 1) / 2;
        int mirrorCalc = KernelSize - 1;
        int length = Output.length - 1;
        int height = Output[0].length - 1;
        
        for (int i = KernelBorder; i < ImageRows - KernelBorder; i++) {
            for (int j = KernelBorder; j < ImageCols  - KernelBorder; j++) {
                double pixel = 0; 
                
                for (int x = 0; x < KernelSize; x++) {
                    for (int y = 0; y < KernelSize; y++) {
                        pixel += I[i + x - KernelBorder][j + y - KernelBorder] * 
                                 kernel[x][y];
                    }
                }
                Output[i][j] = pixel;
            }
        }
        SetCorrectEdges(KernelBorder, Output, mirrorCalc, length, height);
        normalize(Output);
        return Output;
    }
    
    public static double[][] gaussianBlur(int radius)
    {
        int size = radius * 2 + 1;
        double[][] kernel = new double[size][size];
        
        double sigma = radius / 3.0f;
        double twoSigmaSquare = 2.0f * sigma * sigma;
        double sigmaRoot = Math.sqrt(Math.PI * twoSigmaSquare); 
        double total = 0;
     
        for(int i = -radius ; i <= radius ; i++)
        {
            for(int j = -radius; j <= radius ; j++)
            {
                kernel[i + radius][j + radius] = Math.exp(-1 * ((i*i)+(j*j)) / twoSigmaSquare) / sigmaRoot;
                total += kernel[i + radius][j + radius];
            }
        }
        
        // divide the gaussian by total (normalize)
        for(int i = 0 ; i < size ; i++)
        {
            for(int j = 0 ; j < size ; j++)
            {
                kernel[i][j] = kernel[i][j] / total; 
            }
        }
        return kernel;
    }
    
    public static BufferedImage convolve(BufferedImage img, double[][] kernel)
    {    
        return gray2rgb(convolve(rgb2gray(img), kernel));
    }
    
    /**
     * Bilateral Smooth
     * Sets every thing to the correct place
     * @param img
     * @return
     */
    public static BufferedImage SmoothImage(BufferedImage img, int sigma, int iterations)
    {
        double[][] imgArray = rgb2gray(img);
        int KernelSize = sigma * 2 + 1;
        int KernelBorder = (KernelSize - 1) / 2;
        double[][] output = new double[img.getWidth()][img.getHeight()];
        
        for (int iters = 0; iters < iterations; iters++)
        {
            // go over the img 
            for (int i = KernelBorder; i < imgArray.length - KernelBorder; i++) {
                for (int j = KernelBorder; j < imgArray[0].length - KernelBorder; j++) {
                    
                    double total = 0;
                    // build a bilateral filter for the pixel
                    double[][] gaus = new double[KernelSize][KernelSize];
                    
                    double sigmagaus = sigma / 3.0f;
                    double twoSigmaSquare = 2.0f * sigmagaus * sigmagaus;
                    double sigmaRoot = Math.sqrt(Math.PI * twoSigmaSquare); 
                    
                    for(int x = -sigma ; x < sigma; ++x) {
                        for(int y = -sigma ; y < sigma; ++y) {        
                            gaus[x + sigma][y + sigma] = (Math.exp(-1 * ((x*x)+(y*y)) / twoSigmaSquare) / sigmaRoot) * 
                                (imgArray[i][j] - imgArray[i + x][j + y]);
                            total += gaus[x + sigma][y + sigma];
                        }
                    }
                    
                    for(int k = 0 ; k < KernelSize ; k++)
                    {
                        for(int l = 0 ; l < KernelSize ; l++)
                        {
                            gaus[k][l] /= total; 
                        }
                    }
                    
                    // run the filter over the image, as done in convolve
                    double pixel = 0;
                    
                    for (int x = 0; x < KernelSize; x++) {
                        for (int y = 0; y < KernelSize; y++) {
                            pixel += imgArray[i + x - KernelBorder][j + y - KernelBorder] * 
                                gaus[x][y];
                        }
                    }
                    output[i][j] = pixel;
                }
            }
        }
        return gray2rgb(output);
    }

    /**
     * Converts an RGB image object to intensity matrix representation.
     * 
     * @param img RGB buffered image (values [0..255])
     * @return 2D array with unbounded values
     */
    public static double[][] rgb2gray(BufferedImage img)
    {
        int cols = img.getWidth();
        int rows = img.getHeight();

        double[][] A = new double[rows][cols];
        
        for(int i=0;i<rows;++i)
            for(int j=0;j<cols;++j){
                Color c = new Color(img.getRGB(j,i));
                A[i][j] = (c.getRed()+c.getGreen()+c.getBlue())/(3.0*255.0);                
            }
        return A;
    }
    
    public static BufferedImage getImage(double[][] img)
    {
        int height = img.length;
        int width = img[0].length;
        
        BufferedImage retVal = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        
        for(int x=0;x<width;++x)
            for(int y=0;y<height;++y){
                int val = (int) Math.floor(img[y][x]*255);
                val = Math.min(Math.max(val, 0),255);
                retVal.setRGB(x,y,(new Color(val,val,val)).getRGB());
            }
        return retVal;
    }

    /**
     * Converts an intensity matrix to RGB image object. Values clipped between
     * [0..1]
     * 
     * @param A
     *            Intensity matrix with unbounded values
     * @return BufferedImage containing the input monochromic image in each
     *         channel
     */
    public static BufferedImage gray2rgb(double[][] A)
    {
        int height = A.length;
        int width = A[0].length;
        
        BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        
        for(int x=0;x<width;++x)
            for(int y=0;y<height;++y){
                int val = (int) Math.floor(A[y][x]*255);
                val = Math.min(Math.max(val, 0),255);
                img.setRGB(x,y,(new Color(val,val,val)).getRGB());
            }
        return img;
        
    }

    /**
     * Given a grayscale image should perform the edge detection algorithm
     * 
     * @param I Input grayscale intensity image
     * @return New image with marked edges. Should have positive values.
     */
    public static double[][] sobelEdgeDetect(double[][] I)
    {
        
        double kerX[][] = {{-1, 0, 1},
                          {-2, 0, 2},
                          {-1, 0, 1}};
        double kerY[][] = {{-1, -2, -1},
                             {0,   0,  0},
                             {1,   2,  1}};
        double retVal[][] = new double[I.length][I[0].length];
        

        for(int col = 0; col < I.length-1; col++) {
            for (int row = 0; row < I[col].length-1; row++) {
                double newPixelX = 0;
                double newPixelY = 0;
                for (int x = 0; x <= 2; x++) {
                    for (int y = 0; y <= 2; y++) {
                        newPixelX += kerX[x][y] * I[Math.abs(col + x - 1)][Math.abs(row + y - 1)];
                        newPixelY += kerY[x][y] * I[Math.abs(col + x - 1)][Math.abs(row + y - 1)];
                    }
                }
                double newPixel = (Math.abs(newPixelX) + Math.abs(newPixelY));
                retVal[col][row] = newPixel;
            }
        }
        
        /*
        double[][] derivativeX = convolve(I, kerX);
        double[][] derivativeY = convolve(I, kerY);

        for(int col = 0; col < I.length-1; col++) {
            for (int row = 0; row < I[col].length-1; row++) {            
                retVal[col][row] = Math.sqrt(Math.pow(derivativeX[col][row], 2) + Math.pow(derivativeY[col][row], 2)) - I[col][row];
            }
        } */
        normalize(retVal);
        return retVal;
    }

    /**
     * adds the edges to the image 
     * 
     * @remark you can use only one of the co0lors in the pixel to determine the color
     * @param img
     * @param edgeImg
     */
    public static BufferedImage addEdges(BufferedImage img, BufferedImage edgeImg, int threshold)
    {
        return null;
    }
    
    private static void normalize(double[][] A) {
        int rows = A.length;
        int cols = A[0].length;
        
        double max = Double.MIN_VALUE;
        double min = Double.MAX_VALUE;
        
        for (int i = 0; i < rows; i++) {
            for(int j = 0; j < cols; j++) {
                if(A[i][j] >= max)
                    max = A[i][j];
                if(A[i][j] <= min)
                    min = A[i][j];
            }
            
        }
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                A[i][j] = (A[i][j] - min) / (max - min);
            }
        }
    }
    
    private static BufferedImage normalize(BufferedImage img) {
        BufferedImage retVal = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_RGB);
        int rows = img.getWidth();
        int cols = img.getHeight();
        
        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;
        
        for (int i = 0; i < rows; i++) {
            for(int j = 0; j < cols; j++) {
                if (img.getRGB(i, j) >= max) {
                    max = img.getRGB(i, j);
                }
                if (img.getRGB(i, j) <= min) {
                    min = img.getRGB(i, j);
                }
            }
        }
        for (int x = 0; x < rows; x++) {
            for (int y = 0; y < cols; y++) {
                retVal.setRGB(x, y, (img.getRGB(x, y) - min) / (max - min));
            }
        }
        return retVal;
    }
    
    private static void SetCorrectEdges(int edgeSize, double[][] output,
            int mirrorCalc, int length, int height)
    {
        // Using a mirror algorithm to fill the edges:
        for (int i = 0; i < edgeSize; i++)
        {
            for (int j = edgeSize; j < output.length - edgeSize; j++)
            {
                output[j][i] = output[j][mirrorCalc - i];
                output[j][height - i] = output[j][height - mirrorCalc + i];
            }
        }
        
        for (int i = 0; i < edgeSize; i++)
        {
            for (int j = 0; j < output[0].length; j++)
            {
                output[i][j] = output[mirrorCalc - i][j];
                output[length - i][j] = output[length - mirrorCalc + i][j];
            }
        }
    }
}

